﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;
using System.Globalization;

namespace Elentok.Common
{
  /// <summary>
  /// Various SevenZip utility methods (uses the "7zG.exe" or "7z.exe" executable)
  /// </summary>
  public static class SevenZip
  {
    private static string m_ExecutablePath;

    /// <summary>
    /// Gets or sets the path to the "7zG.exe" or "7z.exe" path
    /// </summary>
    public static string ExecutablePath
    {
      get { return m_ExecutablePath; }
      set
      {
        if (value != null && !File.Exists(value))
        {
          string message = string.Format(
            CultureInfo.CurrentCulture,
            "The file '{0}' cannot be found.", value);

          throw new FileNotFoundException(message, value);
        }
        
        m_ExecutablePath = value;
      }
    }

    static SevenZip()
    {
      ExecutablePath = AppConfig.GetString("7z", null);
    }

  }

  /// <summary>
  /// A helper class for running 7-zip
  /// </summary>
  public sealed class SevenZipCommand
  {
    /// <summary>
    /// Initializes an instance of the <see cref="SevenZipCommand"/> class
    /// </summary>
    public SevenZipCommand()
    {
      this.Excludes = new List<string>();
    }

    /// <summary>
    /// Gets or sets the type of this command
    /// </summary>
    public SevenZipCommandType CommandType { get; set; }

    /// <summary>
    /// Gets or sets the arguments of this command
    /// </summary>
    public SevenZipArguments Arguments { get; set; }

    /// <summary>
    /// Gets or sets the source of this command
    /// </summary>
    public string Source { get; set; }

    /// <summary>
    /// Gets or sets the target of the command
    /// </summary>
    public string Target { get; set; }

    /// <summary>
    /// Gets or sets the password of the command
    /// </summary>
    public string Password { get; set; }

    /// <summary>
    /// Gets or sets the lists of patterns to exclude
    /// </summary>
    public IList<string> Excludes { get; private set; }

    /// <summary>
    /// Returns the argument string that will be sent to the 7zip executable
    /// </summary>
    /// <param name="censorPassword">If true and a password was defined, it will appear as "***" marks</param>
    /// <returns></returns>
    public string GetArgumentsString(bool censorPassword)
    {
      StringBuilder builder = new StringBuilder();
      AddCommandType(this.CommandType, builder);
      AddArguments(this.Arguments, builder);
      #region AddPassword
      if (!string.IsNullOrEmpty(Password))
      {
        builder.Append(" -p");
        if (censorPassword)
          builder.Append("****");
        else
          builder.Append(Password);

        // encrypt headers (so without the password you can't even see the filenames)
        builder.Append(" -mhe");
      }
      #endregion
      #region AddExcludes
      if (Excludes != null)
      {
        foreach (string exclude in Excludes)
        {
          builder.Append(" -xr!" + exclude);
        }
      }
      #endregion

      builder.Append(" ");
      builder.Append(this.Target);

      builder.Append(" ");
      builder.Append(this.Source);
      return builder.ToString();
    }

    /// <summary>
    /// Runs the command (returns true if sucessful, false otherwise)
    /// </summary>
    /// <returns></returns>
    public SevenZipExitCode Run()
    {
      ProcessStartInfo info = new ProcessStartInfo(SevenZip.ExecutablePath);

      info.Arguments = GetArgumentsString(false);

      info.CreateNoWindow = false;
      Process process = Process.Start(info);
      process.WaitForExit();

      return (SevenZipExitCode)process.ExitCode;
    }

    #region Helper Methods
    private static void AddCommandType(SevenZipCommandType command, StringBuilder builder)
    {
      switch (command)
      {
        case SevenZipCommandType.Update:
          builder.Append(" u");
          break;
      }
    }

    private static void AddArguments(SevenZipArguments arguments, StringBuilder builder)
    {
      if ((arguments & SevenZipArguments.AllowDeletingFiles) == SevenZipArguments.AllowDeletingFiles)
        builder.Append(" -ms=off");
      if ((arguments & SevenZipArguments.CopySharedFiles) == SevenZipArguments.CopySharedFiles)
        builder.Append(" -ssw");
      if ((arguments & SevenZipArguments.DeleteRemovedFiles) == SevenZipArguments.DeleteRemovedFiles)
        builder.Append(" -uq0p0");

      if ((arguments & SevenZipArguments.Recursive) == SevenZipArguments.Recursive)
        builder.Append(" -r");
      else
        builder.Append(" -r-");
    }
    #endregion

  }

  /// <summary>
  /// The result of a seven zip command
  /// </summary>
  public enum SevenZipExitCode
  {
    /// <summary>
    /// Successful
    /// </summary>
    NoError = 0,
    /// <summary>
    /// Warning
    /// </summary>
    Warning = 1,
    /// <summary>
    /// Fatal error
    /// </summary>
    FatalError = 2,
    /// <summary>
    /// Command line error
    /// </summary>
    CommandLineError = 7,
    /// <summary>
    /// Not enough memory
    /// </summary>
    NotEnoughMemory = 8,
    /// <summary>
    /// User abort
    /// </summary>
    UserStoppedTheProcess = 255
  }

  /// <summary>
  /// The type of command
  /// </summary>
  public enum SevenZipCommandType
  {
    /// <summary>
    /// Update an existing archive
    /// </summary>
    Update
  }

  /// <summary>
  /// The arguments with which to run 7-zip
  /// </summary>
  [Flags]
  public enum SevenZipArguments
  {
    /// <summary>
    /// No arguments
    /// </summary>
    None = 0,
    /// <summary>
    /// go into subdirectories (-r, otherwise "-r-")
    /// </summary>
    Recursive = 1,
    /// <summary>
    /// copy shared files (open-for-writing) (-ssw)
    /// </summary>
    CopySharedFiles = 2,
    /// <summary>
    /// create non-solid archive to allow deleting files (-ms=off)
    /// </summary>
    AllowDeletingFiles = 4,
    /// <summary>
    /// delete removed files (and files that no longer match the exclude list) (-uq0p0)
    /// </summary>
    DeleteRemovedFiles = 8,
  }
}
